package p2p;

import com.sun.corba.se.impl.orbutil.concurrent.Mutex;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.security.NoSuchAlgorithmException;
import java.util.StringTokenizer;
import java.util.List;
import java.net.DatagramPacket;
import java.util.LinkedList;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JOptionPane;

/** Class that represents a file being shared.
 * @author Bruno
 */
public class P2PFile {

	/** File is being downloaded. */
	static public final int STATE_DOWNLOADING = 1;
	/** User has paused downloading the file. */
	static public final int STATE_PAUSED = 2;
	/** File has been fully downloaded. */
	static public final int STATE_COMPLETE = 3;

	/** Creates a new P2PFile (a file that exists in the local machine).
	 * @param file Existing file in the file system.
	 */
	public P2PFile(File file) throws FileNotFoundException, NoSuchAlgorithmException, IOException {
		name = file.getName();
		path = file.getAbsolutePath();
		size = file.length();
		sizeHumanReadable = Global.sizeToHumanReadable(size);
		hash = Global.calculateCheckSum(path);

		// Divide file in chunks
		totalNumChunks = availableNumChunks = (int)((size - 1) / Global.chunkSize) + 1;
		availableChunks = new boolean[totalNumChunks];
		for(int i = 0; i < totalNumChunks; i++)
			availableChunks[i] = true;

		state = STATE_COMPLETE;

        this.file = new RandomAccessFile(path, "r");
	}

	/** Creates a P2PFile with the information from a SearchFile (a remote file in the network).
	 * @param file Information of a file in the network, obtained with a SEARCH message.
	 * @param path Where to save the file in the local machine.
	 * @throws IOException If an error occurred while creating the file.
	 */
	public P2PFile(SearchFile file, String path) throws IOException {
		name = file.getName();
		this.path = path;
		size = file.getSize();
		sizeHumanReadable = file.getHumanReadableSize();
		hash = file.getHash();

		// Divide file into chunks
		totalNumChunks = (int)((size - 1) / Global.chunkSize) + 1;
		availableNumChunks = 0;
		availableChunks = new boolean[totalNumChunks];
		for(int i = 0; i < totalNumChunks; i++)
			availableChunks[i] = false;

		state = STATE_DOWNLOADING;

		this.file = new RandomAccessFile(path, "rw");
		this.file.setLength(size);
	}

    public void deleteIfIncomplete() {
		fileThread.requestStop();
		File f = new File(path);
		try {
			file.close();
		} catch (IOException ex) {
			return;
		}

        if(!isComplete()) {
            try {
                f.delete();
            }
            catch(Exception ex) {
                return;
            }
        }
    }

	/** Returns the name of the file.
	 * @return File's name.
	 */
	public String getName() {
		return name;
	}

	/** Returns the path to the file on the local computer.
	 * @return File's path.
	 */
	public String getPath() {
		return path;
	}

	/** Returns the total size of the file in bytes.
	 * @return File's size.
	 */
	public long getSize() {
		return size;
	}

	/** Returns the file's size in human-readable format.
	 * @return String containing the size of the file in human-readable format.
	 */
	public String getHumanReadableSize() {
		return sizeHumanReadable;
	}

	/** Returns the file's hash value.
	 * @return File's hash value.
	 */
	public String getHash() {
		return hash;
	}

	/** Returns the total number of chunks in the file.
	 * @return File's total number of chunks.
	 */
	public int getTotalNumChunks() {
		return totalNumChunks;
	}

	/** Returns the number of available (fully transfered) chunks in the file.
	 * @return File's number of available chunks.
	 */
	public int getAvailableNumChunks() {
		return availableNumChunks;
	}

	/** Returns the progress of the transfer (0-100).
	 * @return Transfer progress.
	 */
	public int getTransferProgress() {
		return (int)((float)availableNumChunks / (float)totalNumChunks * 100.0);
	}

	/** Returns if the file is complete in the local machine.
	 * @return <b>true</b> if the file has been fully downloaded.
	 */
	public boolean isComplete() {
		return availableNumChunks == totalNumChunks;
	}

	/** Returns if the chunk with the specified <i>index</i> is available (fully transfered).
	 * @param index Index of the chunk.
	 * @return True if the chunk is available.
	 */
	public boolean isChunkAvailable(int index) {
        if (!isValueChunkNumber(index))
			return false;
		else
			return availableChunks[index];
	}

    /** Returns <b>true</b> if chunkNumber isn't negative and isn't bigger than the number of chunks avaiable
     * @param chunkNumber The number to check
     * @return <b>true</b> if chunkNumber is valid, otherwise return <i>false</i>
     */
    public boolean isValueChunkNumber(int chunkNumber) {
        if (chunkNumber < 0 || chunkNumber >= getTotalNumChunks())
            return false;
        else
            return true;
    }

	/** Returns if this file has any chunk fully available.
	 * @return <b>true</b> if any chunk is available.
	 */
	public boolean hasChunksAvailable() {
		if(getAvailableNumChunks() > 0)
			return true;
		else
			return false;
	}

    public boolean isLastIndex(int index) {
        return index == totalNumChunks - 1;
    }

	/** Returns if all the keywords in the filter are found on the file name.
	 * @param filter String containing the keywords to search in the file name.
	 * @return <b>true</b> if the file name matches the filter.
	 */
	public boolean matchName(String filter) {
		filter = filter.toLowerCase(); // case-insensitive match
		String tempName = name.toLowerCase();
		String keyword;
		StringTokenizer tok = new StringTokenizer(filter);

		while(tok.hasMoreTokens()) {
			keyword = tok.nextToken();
			if(!tempName.contains(keyword))
				return false;
		}

		return true; // all keywords were found on the filename
	}

	/** Returns the state of the download.
	 * @return <i>STATE_DOWNLOADING</i>, <i>STATE_PAUSE</i> or <i>STATE_COMPLETE</i>.
	 */
	public int getState() {
		return state;
	}

	/** Resumes downloading/sharing this file.
	 */
	public void resumeDownload() {
		if(state == STATE_COMPLETE || state == STATE_DOWNLOADING)
			return;
		else {
			fileThread = new P2PFileThread();
			startThread();
			if(isComplete()) {
				state = STATE_COMPLETE;
			}
			else {
				state = STATE_DOWNLOADING;
			}
		}
	}

	/** Pauses downloading/sharing this file.
	 */
	public void pauseDownload() {
		if(state == STATE_COMPLETE || state == STATE_DOWNLOADING) {
			fileThread.requestStop();
			state = STATE_PAUSED;
		}
	}

    /** Ask the network to retrieve all chunks of this file
     */
    public void askForChunk() {
        try {
            askForChunk(0, totalNumChunks - 1);
        }
        catch (ChunkOutOfBoundsException e) {
            // This shouldn't happen
        }
    }

    /** Ask the network to retrieve a specific chunk of this file
     * @param chunkNumber The number of the chunk to retrieve
     * @throws ChunkOutOfBoundsException If the chunkNumber doesn't match a valid chunk
     */
    public void askForChunk(int chunkNumber) throws ChunkOutOfBoundsException {
        if (!isValueChunkNumber(chunkNumber)) {
            throw new ChunkOutOfBoundsException();
        }

        String getCommand = HelperGetFunctions.constructGetCommand(getHash(), chunkNumber);

		// Send GET to the network
		DatagramPacket packet = new DatagramPacket(getCommand.getBytes(), getCommand.length(),
												   Global.multicastInetAddr, Global.controlPort);
		try {
			Global.controlSocket.send(packet);
		} catch (Exception e) {
            e.printStackTrace();
            System.err.println("Error sending GET command");
        }
    }

    /** Ask the network to retrieve a range of chunks of this file
     * @param leftValue The lower bound of the range
     * @param rightValue The upper bound of the range
     * @throws ChunkOutOfBoundsException If there are any values in the range that doesn't match a valid chunk
     */
    public void askForChunk(int leftValue, int rightValue) throws ChunkOutOfBoundsException {
        if (leftValue == rightValue) {
            // A mesma coisa que pedir so um chunk
            askForChunk(leftValue);
        }
        if (leftValue > rightValue) {
            throw new ChunkOutOfBoundsException();
        }
        if (!isValueChunkNumber(leftValue) || !isValueChunkNumber(rightValue)) {
            throw new ChunkOutOfBoundsException();
        }

        String getCommand = HelperGetFunctions.constructGetCommand(getHash(), leftValue, rightValue);

		// Send GET to the network
		DatagramPacket packet = new DatagramPacket(getCommand.getBytes(), getCommand.length(),
												   Global.multicastInetAddr, Global.controlPort);
		try {
			Global.controlSocket.send(packet);
		} catch (Exception e) {
            e.printStackTrace();
            System.err.println("Error sending GET command");
		}
    }

    /** Ask the network to retrieve a group of specific chunks of this file
     * @param chunks A list of chunks to retrieve
     * @throws ChunkOutOfBoundsException If there's a non-existent chunk in <i>chunks</i>
     */
    // FIXME: O parametro chunks devia de ser talvez um Set<int> para evitar os valores em duplicado
    public void askForChunk(List<Integer> chunks) throws ChunkOutOfBoundsException {
        // Check if every chunk is a valid number
        for (int chunk : chunks) {
            if (!isValueChunkNumber(chunk)) {
                throw new ChunkOutOfBoundsException();
            }
        }

        String getCommand = HelperGetFunctions.constructGetCommand(getHash(), chunks);

		// Send GET to the network
		DatagramPacket packet = new DatagramPacket(getCommand.getBytes(), getCommand.length(),
												   Global.multicastInetAddr, Global.controlPort);
		try {
			Global.controlSocket.send(packet);
		} catch (Exception e) {
            e.printStackTrace();
            System.err.println("Error sending GET command");
		}
    }

	/** Ask the network to retrieve remaing chunks of the file.
	 */
	public void askForRemainingChunks() {
		List<Integer> l = new LinkedList<Integer>();
		for(int i = 0; i < availableChunks.length; i++)
			if(availableChunks[i] == false)
				l.add(i);

		if(!l.isEmpty()) {
			try{
				askForChunk(l);
			} catch (ChunkOutOfBoundsException ex) { }
		}
	}

    public byte[] getChunk(int index) {
        if (isChunkAvailable(index)) {
            try {
                file.seek(index * Global.chunkSize);

				int chunkSize = Global.chunkSize;
				if(isLastIndex(index))
					chunkSize = (int)getSizeLastChunk();
                byte[] buffer = new byte[chunkSize];

                int r = file.read(buffer);


                return buffer;
            } catch (IOException ex) {
                return null;
            }

        }
        else
            return null;
    }

    public void writeChunk(int index, byte[] data) {
        try {
            file.seek(index * Global.chunkSize);

            if (isLastIndex(index)) {
                file.write(data, 0, (int) getSizeLastChunk());
            } else {
                file.write(data);
            }

            availableChunks[index] = true;
            availableNumChunks++;

			if(isComplete()) { // File is complete? Change state to "Complete"
				state = STATE_COMPLETE;
				if(Global.frmP2P != null)
					Global.frmP2P.setFileComplete(P2PFileList.getFileIndex(this));
			}

        } catch (Exception ex) {
            return;
        }
    }

    public long getSizeLastChunk() {
        return size % Global.chunkSize;
    }

	@Override
	public boolean equals(Object obj) {
		// Both name and hash values are compared
		if(obj instanceof P2PFile) {
			P2PFile f = (P2PFile)obj;
			return f.name.equals(name) || f.hash.equals(hash);
		}
		else
			return false;
	}


	/** Add a request for the chunk with the specified <i>index</i> to the list of requested chunks.<br>
	 * The request is not added if the chunk is not available.
	 * @param index Index of the requested chunk.
	 */
	public void addRequestedChunk(Integer index) {
		if(isValueChunkNumber(index) && isChunkAvailable(index)) {
			try {
				requestedChunksMutex.acquire();
			} catch (InterruptedException ex) { }
			requestedChunks.add(index);
			requestedChunksMutex.release();
		}
	}

	/** Add requests for all the chunks with the indexes specified in the given list.
	 * @param indexes List of indexes of the requested chunks.
	 */
	public void addRequestedChunks(List<Integer> indexes) {
		for(Integer index: indexes)
			addRequestedChunk(index);
	}

	/** Removes a request for the chunk with the specified <i>index</i> from the list of requested chunks.
	 * @param index Index of the requested chunk.
	 */
	public void removeRequestedChunk(Integer index) {
		try {
			requestedChunksMutex.acquire();
		} catch (InterruptedException ex) { }
		requestedChunks.remove(index);
		requestedChunksMutex.release();
	}

	/** Returns if the chunk with the indicated <i>index</i> has been requested by other peers.
	 * @param index Index of the requested chunk.
	 * @return <b>true</b> if the chunk has been requested.
	 */
	public boolean isChunkRequested(Integer index) {
		try {
			requestedChunksMutex.acquire();
		} catch (InterruptedException ex) { }
		boolean is = requestedChunks.contains(index);
		requestedChunksMutex.release();
		return is;
	}

	/** Returns if any chunk of this file has been requested.
	 * @return <b>true</b> if any chunk of this file has been requested.
	 */
	public boolean hasRequestedChunks() {
		return !requestedChunks.isEmpty();
	}

	/** Randomly pick an index from the requested chunks.
	 * @return Random requested chunk index.
	 */
	public Integer getRandomRequestedChunkIndex() {
		if(requestedChunks.isEmpty())
			return -1;

		try {
			requestedChunksMutex.acquire();
		} catch (InterruptedException ex) { }
		Integer[] aux = new Integer[requestedChunks.size()];
		requestedChunks.toArray(aux);
		requestedChunksMutex.release();
		return aux[(int)(Math.random() * (double)aux.length)];
	}

	/** Start running this file thread.
	 */
	public void startThread() {
		fileThread.start(this);
	}

	/** Add a received chunk to the list of received chunks to be processed later.
	 * @param netChunk Chunk to add.
	 */
	public void addReceivedChunk(NetworkChunk netChunk) {
		try {
			receivedChunksMutex.acquire();
		} catch (InterruptedException ex) { }
		receivedChunks.offer(netChunk);
		receivedChunksMutex.release();
	}

	/** Return if the file has received chunks waiting to be processed (written to disk).
	 * @return <b>true</b> if the file has received chunks.
	 */
	public boolean hasReceivedChunks() {
		return !receivedChunks.isEmpty();
	}

	/** Retrieve and remove the next received chunk.
	 * @return Next received chunk.
	 */
	public NetworkChunk popReceivedChunk() {
		try {
			receivedChunksMutex.acquire();
		} catch (InterruptedException ex) { }
		NetworkChunk n = receivedChunks.poll();
		receivedChunksMutex.release();
		return n;
	}

	private String name = "";
	private String path = "";
	private long size = 0;
	private String sizeHumanReadable = "";
	private String hash = null;
	private int totalNumChunks = 0;
	private int availableNumChunks = 0;
	private boolean availableChunks[];
    private RandomAccessFile file;
	private int state = STATE_PAUSED;
	private TreeSet<Integer> requestedChunks = new TreeSet<Integer>();
	private Mutex requestedChunksMutex = new Mutex();
	private P2PFileThread fileThread = new P2PFileThread();
	private LinkedList<NetworkChunk> receivedChunks = new LinkedList<NetworkChunk>();
	private Mutex receivedChunksMutex = new Mutex();
}

class ChunkOutOfBoundsException extends Exception {}
